﻿using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using Skugo.Shared;

namespace Skugo
{
    public class Connection : IReader, IWriter
    {
        /// <summary>
        /// The socket which is connected.
        /// </summary>
        private Socket Socket;

        /// <summary>
        /// A concurrent queue of all incoming data (we read data from another thread, and throw it on here).
        /// </summary>
        private ConcurrentQueue<Byte[]> Incoming = new ConcurrentQueue<Byte[]>();

        // The receive buffer we use to receive data
        // This buffer is only allocated once, but is used on a different thread
        // other than the one the client was created on
        private Byte[] ReceiveBuffer;

        /// <summary>
        /// Tells us whether this connection is still currently connected.
        /// </summary>
        public Boolean IsConnected
        {
            get { return this.Socket != null && this.Socket.Connected; }
        }

        /// <summary>
        /// Checks if the connection is locally connected.
        /// </summary>
        public Boolean IsLocal
        {
            get
            {
                // Get the end-point as an ip end point
                var ip = this.Socket.RemoteEndPoint as IPEndPoint;

                // If it was indeed an ip...
                if (ip != null)
                {
                    // Just check to see if it was the loopback
                    return IPAddress.IsLoopback(ip.Address);
                }

                // Otherwise, we may be connected by name
                var dns = this.Socket.RemoteEndPoint as DnsEndPoint;
                
                // Check if we got a valid dns end point
                if (dns != null)
                {
                    try
                    {
                        // The end point was valid, but now we need to resolve it
                        var entry = Dns.GetHostEntry(dns.Host);

                        // Either we failed by here, or it succeeded
                        // Loop through all the resolved addresses
                        for (Int32 i = 0; i < entry.AddressList.Length; ++i)
                        {
                            // If any of these addresses are the loopback addresss
                            if (IPAddress.IsLoopback(entry.AddressList[i]))
                            {
                                // Return that we are local
                                return true;
                            }
                        }
                    }
                    catch
                    {
                    }
                }

                // Otherwise if we got here, we failed!
                return false;
            }
        }

        /// <summary>
        /// Store information about data we're trying to send.
        /// </summary>
        private class SendData
        {
            /// <summary>
            /// The data that is being sent.
            /// </summary>
            public Byte[] Data;

            /// <summary>
            /// The amount of data we've already sent.
            /// </summary>
            public Int32 AmountSent = 0;
        }

        /// <summary>
        /// Creates a new connection which manages sending and receiving to a socket.
        /// </summary>
        public Connection(Socket socket)
        {
            // Store the socket away
            this.Socket = socket;

            // Attempt to set the receive buffer size
            socket.ReceiveBufferSize = 7680000;

			// Create the receive buffer based on the max data we can receive
            this.ReceiveBuffer = new Byte[socket.ReceiveBufferSize];

            // Start receiving data
            socket.BeginReceive(this.ReceiveBuffer, 0, this.ReceiveBuffer.Length, SocketFlags.None, ReceiveResult, null);
        }

        // Occurs when we receive data from the socket
        private void ReceiveResult(IAsyncResult ar)
        {
            // The number of bytes we received
            Int32 bytesReceived;

            try
            {
                // Get the number of bytes received
                bytesReceived = this.Socket.EndReceive(ar);
            }
            catch (SocketException)
            {
                // We are no longer connected due to a socket closure
                this.Disconnect();
                return;
            }

            // Create a new buffer to hold all the received data
            Byte[] newBuffer = new Byte[bytesReceived];

            // Copy the received data into the new buffer
            Buffer.BlockCopy(this.ReceiveBuffer, 0, newBuffer, 0, bytesReceived);

            // Add the new buffer to the thread safe queue of incoming data
            // (Right now, we're not on the same thread as the object was created)
            this.Incoming.Enqueue(newBuffer);

            try
            {
                // Start receiving data again
                this.Socket.BeginReceive(this.ReceiveBuffer, 0, this.ReceiveBuffer.Length, SocketFlags.None, this.ReceiveResult, null);
            }
            catch (SocketException)
            {
                // We are no longer connected due to a socket closure
                this.Disconnect();
                return;
            }
        }

        /// <summary>
        /// Disconnect from the client.
        /// </summary>
        public void Disconnect()
        {
            // If the socket is already null, then exit out early
            if (this.Socket.Connected == false)
                return;

            // Shutdown and close the socket
            // Unlike the C++ side, we do not need to wait for the threads to close because the actual
            // threads themselves are created by the socket, and are therefore canceled by closing the socket
            this.Socket.Shutdown(SocketShutdown.Both);
            this.Socket.Close();
        }

        public Byte[] Read()
        {
            // Store a byte array to read data into
            Byte[] readData = null;

            // If we have pending data, and if we have listeners...
            if (this.Incoming.TryDequeue(out readData))
            {
                // Returning the pending data
                return readData;
            }
            
            // Otherwise, return null since no data was read
            return null;
        }

        public void Write(Byte[] data)
        {
            // Note: Sending does not work the same way it does in C++
            // In the C++ version, we have another concurrent queue which put data onto
            // and a running thread will send the data when it sees it on the queue
            // In the C#/.NET version they have a simple function which will send data
            // and async callback when the data is sent, a much more straightforward approach

            // Only send data if we're connected...
            if (this.IsConnected)
            {
                try
                {
                    // Asynchronously send the data
                    this.Socket.BeginSend(data, 0, data.Length, SocketFlags.None, this.SendResult, new SendData() { Data = data });
                }
                catch (SocketException)
                {
                    // If we encountered an exception, the socket was probably closed
                    this.Disconnect();
                    return;
                }
            }
        }

        private void SendResult(IAsyncResult ar)
        {
            // Get the send data from the state
            var sendData = (SendData)ar.AsyncState;

            try
            {
                // Get the number of bytes sent
                Int32 bytesSent = this.Socket.EndSend(ar);

                // Track how many bytes were sent out of the whole data packet
                sendData.AmountSent += bytesSent;

                // If we haven't sent the entire packet...
                if (sendData.AmountSent < sendData.Data.Length)
                {
                    // Compute the amount of data we have left to send
                    Int32 dataLeft = sendData.Data.Length - sendData.AmountSent;

                    // Attempt to send the data again asynchronously
                    this.Socket.BeginSend(sendData.Data, sendData.AmountSent, dataLeft, SocketFlags.None, SendResult, sendData);
                }
            }
            catch (SocketException)
            {
                // If we encountered an exception, the socket was probably closed
                this.Disconnect();
                return;
            }
        }
    }

    public class Host : IWriter
    {
        /// <summary>
        /// The actual client we'll use to manage the connection.
        /// </summary>
        private Socket Socket;

        /// <summary>
        /// All the recent connections (these connections are coming from another thread).
        /// </summary>
        private ConcurrentQueue<Connection> NewConnections = new ConcurrentQueue<Connection>();

        /// <summary>
        /// All the active connections (these connects are on the main thread).
        /// </summary>
        private List<Connection> ActiveConnections = new List<Connection>();

        /// <summary>
        /// Start hosting a session.
        /// </summary>
        /// <param name="port">The port we'd like to host on.</param>
        public void StartHosting(UInt16 port)
        {
            // Create a Tcp stream socket (Ipv4)
            this.Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            // Bind the socket to the given port
            this.Socket.Bind(new IPEndPoint(IPAddress.Any, port));

            // Listen for connections
            this.Socket.Listen((Int32)SocketOptionName.MaxConnections);

            // Start accepting incoming sockets
            this.Socket.BeginAccept(this.AcceptConnection, null);
        }

        private void AcceptConnection(IAsyncResult ar)
        {
            // Get the accepted socket
            var newSocket = this.Socket.EndAccept(ar);

            // If the socket was not valid, exit out early
            if (newSocket == null)
                return;

            // Concurrently add the new connection to a queue
            this.NewConnections.Enqueue(new Connection(newSocket));

            // Keep attempting to accept sockets
            this.Socket.BeginAccept(this.AcceptConnection, null);
        }

        /// <summary>
        /// Stop the hosting (so no more clients can connect), but does not stop the existing or pending connections.
        /// </summary>
        public void StopHosting()
        {
            // If the socket is already invalid, then exit early
            if (this.Socket == null)
                return;

            // Shutdown and close the socket
            // Unlike the C++ side, we do not need to wait for the threads to close because the actual
            // threads themselves are created by the socket, and are therefore canceled by closing the socket
            this.Socket.Shutdown(SocketShutdown.Both);
            this.Socket.Close();

            // Clear out the socket so that any attempt to use it will fail
            this.Socket = null;

            // Note that we don't actually clear the active connections
            // Stopping hosting just means that the socket is no longer accepting connections
        }

        /// <summary>
        /// Gets the next recent connection (or returns NULL).
        /// </summary>
        /// <returns>Returns the connection if one was added, or null.</returns>
        public Connection PopNewConnection()
        {
            // Store the connection that we read from the queue
            Connection newConnection = null;

            // If there is a pending connection...
            if (this.NewConnections.TryDequeue(out newConnection))
            {
                // Add the connection to the list of active connections
                this.ActiveConnections.Add(newConnection);

                // Return the connection that we popped!
                return newConnection;
            }

            // Otherwise there was nothing
            return null;
        }

        public void Write(Byte[] data)
        {
            // Loop through all the active connections
            for (Int32 i = 0; i < this.ActiveConnections.Count;)
            {
                // Grab the current connection
                var connection = this.ActiveConnections[i];

                // If the connection is currently connected...
                if (connection.IsConnected)
                {
                    // Write data out to the socket
                    connection.Write(data);

                    // Iterate to the next socket
                    ++i;
                }
                else
                {
                    // Erase the connection (which will move everything in front)
                    this.ActiveConnections.RemoveAt(i);
                }
            }
        }

        /// <summary>
        /// Close all the connections (including active and pending), but does not stop listening for new connections.
        /// </summary>
        public void CloseAllConnections()
        {
            // Loop through all the active connections
            foreach (var connection in this.ActiveConnections)
            {
                // Disconnect the connection
                connection.Disconnect();
            }

            // Clear the list of connections
            this.ActiveConnections.Clear();

            // Loop through all pending connections
            Connection pending = null;

            // Loop while we have pending connections...
            while ((pending = PopNewConnection()) != null)
            {
                // Disconnect the pending connection
                pending.Disconnect();
            }
        }

        /// <summary>
        /// Stops hosting entirely, and closes all active or pending connections.
        /// </summary>
        public void Shutdown()
        {
            // Stop hosting and close the connections
            this.StopHosting();
            this.CloseAllConnections();
        }
    }
}
